import sys
import os
import json
import shutil
import re
import zipfile
import time
from datetime import datetime
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
    QWidget, QTextEdit, QPushButton, QFileDialog,
    QTreeWidget, QTreeWidgetItem, QSplitter,
    QLabel, QMenuBar, QMenu, QMessageBox,
    QInputDialog, QColorDialog, QProgressDialog,
    QDialog, QFormLayout, QLineEdit, QCheckBox,
    QToolBar, QComboBox, QDoubleSpinBox, QGroupBox,
    QRadioButton, QButtonGroup, QListWidget, QSpinBox
)
from PySide6.QtGui import (
    QColor, QTextCharFormat, QSyntaxHighlighter,
    QFont, QKeyEvent, QAction, QKeySequence,
    QIntValidator, QTextCursor, QTextDocument, QDesktopServices,
    QClipboard, QBrush, QPalette, QIcon, QShortcut
)
from PySide6.QtCore import (
    Qt, QTimer, QUrl, QByteArray, QSize,
    QPropertyAnimation, QEasingCurve, QSequentialAnimationGroup,
    QParallelAnimationGroup, QAbstractAnimation, QMargins
)
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from fpdf import FPDF
from ebooklib import epub
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
import requests
from bs4 import BeautifulSoup

class CustomTextEdit(QTextEdit):
    def __init__(self, parent=None, save_callback=None):
        super().__init__(parent)
        self.save_callback = save_callback
        self.setUndoRedoEnabled(True)

    def keyPressEvent(self, e):
        if e.key() == Qt.Key_S and e.modifiers() == Qt.ControlModifier:
            if self.save_callback:
                self.save_callback()
            return
        elif e.key() == Qt.Key_F and e.modifiers() == Qt.ControlModifier:
            self.parent().show_search_dialog()
            return
        elif e.key() == Qt.Key_Z and e.modifiers() == Qt.ControlModifier:
            self.undo()
            return
        elif e.key() == Qt.Key_Y and e.modifiers() == Qt.ControlModifier:
            self.redo()
            return
        elif e.key() == Qt.Key_F3 and e.modifiers() == Qt.NoModifier:
            self.parent().find_next()
            return
        super().keyPressEvent(e)

class SearchReplaceWidget(QWidget):
    def __init__(self, editor, parent=None):
        super().__init__(parent)
        self.editor = editor
        self.current_index = 0
        self.matches = []
        self.search_history = []
        self.setObjectName("searchWidget")
        self.setStyleSheet("""
            #searchWidget {
                background-color: palette(window);
                border: 1px solid palette(mid);
                border-radius: 2px;
            }
        """)
        main_layout = QHBoxLayout(self)
        main_layout.setContentsMargins(5, 5, 5, 5)
        main_layout.setSpacing(3)
        self.search_input = QComboBox()
        self.search_input.setEditable(True)
        self.search_input.setFixedWidth(150)
        self.search_input.lineEdit().setPlaceholderText("搜索...")
        self.search_input.lineEdit().returnPressed.connect(self.find_next)
        self.find_next_btn = QPushButton("查找")
        self.find_next_btn.setFixedWidth(37)
        self.find_next_btn.clicked.connect(self.find_next)
        self.replace_input = QLineEdit()
        self.replace_input.setFixedWidth(120)
        self.replace_input.setPlaceholderText("替换为...")
        self.replace_btn = QPushButton("替换")
        self.replace_btn.setFixedWidth(37)
        self.replace_btn.clicked.connect(self.replace_current)
        self.replace_all_btn = QPushButton("全部")
        self.replace_all_btn.setFixedWidth(37)
        self.replace_all_btn.clicked.connect(self.replace_all)
        self.options_btn = QPushButton("选项")
        self.options_btn.setFixedWidth(40)
        self.options_btn.setCheckable(True)
        self.options_btn.toggled.connect(self.toggle_options)
        self.options_panel = QWidget()
        options_layout = QHBoxLayout(self.options_panel)
        options_layout.setContentsMargins(0, 0, 0, 0)
        options_layout.setSpacing(5)
        self.case_checkbox = QCheckBox("区分大小写")
        self.word_checkbox = QCheckBox("全字匹配")
        self.loop_checkbox = QCheckBox("循环")
        self.loop_checkbox.setChecked(True)
        self.regex_checkbox = QCheckBox("正则")
        options_layout.addWidget(self.case_checkbox)
        options_layout.addWidget(self.word_checkbox)
        options_layout.addWidget(self.loop_checkbox)
        options_layout.addWidget(self.regex_checkbox)
        self.options_panel.setVisible(False)
        self.close_btn = QPushButton("×")
        self.close_btn.setFixedSize(20, 20)
        self.close_btn.clicked.connect(self.hide)
        main_layout.addWidget(self.search_input)
        main_layout.addWidget(self.find_next_btn)
        main_layout.addWidget(self.replace_input)
        main_layout.addWidget(self.replace_btn)
        main_layout.addWidget(self.replace_all_btn)
        main_layout.addWidget(self.options_btn)
        main_layout.addWidget(self.close_btn)
        main_layout.addWidget(self.options_panel)
        self.hide()

    def toggle_options(self, checked):
        self.options_panel.setVisible(checked)

    def showEvent(self, event):
        self.search_input.lineEdit().setFocus()
        self.search_input.lineEdit().selectAll()
        super().showEvent(event)

    def hideEvent(self, event):
        self.options_btn.setChecked(False)
        super().hideEvent(event)

    def find_all(self):
        search_text = self.search_input.currentText().strip()
        if not search_text:
            return
        if search_text not in self.search_history:
            self.search_history.insert(0, search_text)
            self.search_input.insertItem(0, search_text)
            if len(self.search_history) > 10:
                self.search_history.pop()
                self.search_input.removeItem(10)
        self.editor.setTextCursor(QTextCursor(self.editor.document()))
        self.matches = []
        document = self.editor.document()
        start_cursor = QTextCursor(document)
        start_cursor.movePosition(QTextCursor.Start)
        flags = QTextDocument.FindFlags()
        if self.case_checkbox.isChecked():
            flags |= QTextDocument.FindCaseSensitively
        if self.word_checkbox.isChecked():
            flags |= QTextDocument.FindWholeWords
        if self.regex_checkbox.isChecked():
            try:
                pattern = re.compile(search_text, 0 if not self.case_checkbox.isChecked() else re.IGNORECASE)
                cursor = start_cursor
                while True:
                    match = pattern.search(document.toPlainText(), cursor.position())
                    if not match:
                        break
                    cursor.setPosition(match.start())
                    cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
                    self.matches.append((match.start(), match.end()))
                    cursor.setPosition(match.end())
            except re.error as e:
                QMessageBox.warning(self, "正则错误", f"正则表达式无效: {e}")
                return
        else:
            cursor = start_cursor
            while True:
                cursor = document.find(search_text, cursor, flags)
                if cursor.isNull():
                    break
                start = cursor.selectionStart()
                end = cursor.selectionEnd()
                self.matches.append((start, end))
                cursor.setPosition(end)
        highlight_format = QTextCharFormat()
        highlight_format.setBackground(QBrush(QColor("#FFFF00")))
        for start, end in self.matches:
            cursor = QTextCursor(document)
            cursor.setPosition(start)
            cursor.setPosition(end, QTextCursor.KeepAnchor)
            cursor.setCharFormat(highlight_format)
        if self.matches:
            self.current_index = 0
            self.highlight_match(self.current_index)
        else:
            QMessageBox.information(self, "未找到", f"找不到 '{search_text}'")

    def find_next(self):
        self.search_text = self.search_input.currentText()
        if not self.search_text:
            return
        if self.options_panel.isVisible():
            self.setFixedHeight(60)
        else:
            self.setFixedHeight(30)
        if not self.matches or len(self.matches) == 0:
            self.find_all()
            return
        if self.matches:
            if self.loop_checkbox.isChecked():
                self.current_index = (self.current_index + 1) % len(self.matches)
            else:
                if self.current_index + 1 >= len(self.matches):
                    QMessageBox.information(self, "已到末尾", "已到达搜索结果末尾。")
                    return
                self.current_index += 1
            self.highlight_match(self.current_index)

    def highlight_match(self, index):
        if 0 <= index < len(self.matches):
            start, end = self.matches[index]
            cursor = self.editor.textCursor()
            cursor.setPosition(start)
            cursor.setPosition(end, QTextCursor.KeepAnchor)
            self.editor.setTextCursor(cursor)
            self.editor.ensureCursorVisible()

    def replace_current(self):
        if not self.matches:
            return
        cursor = self.editor.textCursor()
        if cursor.hasSelection() and cursor.selectedText() == self.search_text:
            cursor.insertText(self.replace_input.text())
            self.find_all()

    def replace_all(self):
        search_text = self.search_input.currentText()
        if not search_text:
            return
        document = self.editor.document()
        cursor = QTextCursor(document)
        cursor.beginEditBlock()
        count = 0
        temp_marker = f"__REPLACE_MARKER_{time.time()}__"
        while True:
            cursor = document.find(search_text, cursor)
            if cursor.isNull():
                break
            cursor.insertText(temp_marker)
            count += 1
        cursor.setPosition(0)
        while True:
            cursor = document.find(temp_marker, cursor)
            if cursor.isNull():
                break
            cursor.insertText(self.replace_input.text())
        cursor.endEditBlock()
        QMessageBox.information(self, "替换完成", f"已替换 {count} 处匹配项")
        self.search_input.clear()
        self.matches = []

class SymbolHighlighter(QSyntaxHighlighter):
    def __init__(self, document, config):
        super().__init__(document)
        self.config = config

    def highlightBlock(self, text):
        format_clear = QTextCharFormat()
        self.setFormat(0, len(text), format_clear)
        stack = []
        for i, ch in enumerate(text):
            if ch in "{([":
                stack.append((ch, i, len(stack)))
            elif ch in "})]":
                if stack:
                    start_ch, start_idx, depth = stack.pop()
                    pair = {'{': '}', '(': ')', '[': ']'}
                    if pair[start_ch] == ch:
                        if depth == 0:
                            color = QColor(self.config['first_level_color'])
                        elif depth == 1:
                            color = QColor(self.config['second_level_color'])
                        elif depth >= 2:
                            color = QColor(self.config['third_level_color'])
                        else:
                            color = Qt.black
                        fmt = QTextCharFormat()
                        fmt.setForeground(color)
                        self.setFormat(start_idx, 1, fmt)
                        self.setFormat(i, 1, fmt)
                else:
                    fmt = QTextCharFormat()
                    fmt.setForeground(Qt.red)
                    self.setFormat(i, 1, fmt)
        self.highlight_quotes(text, '"')
        self.highlight_quotes(text, "'")
        self.highlight_chinese(text)

    def highlight_quotes(self, text, quote_char):
        i = 0
        while i < len(text):
            if text[i] == quote_char:
                start = i
                i += 1
                while i < len(text) and text[i] != quote_char:
                    i += 1
                if i < len(text):
                    end = i
                    fmt = QTextCharFormat()
                    fmt.setForeground(QColor(self.config['quote_color']))
                    self.setFormat(start, end - start + 1, fmt)
            i += 1

    def highlight_chinese(self, text):
        fmt = QTextCharFormat()
        fmt.setForeground(QColor(self.config['chinese_color']))
        i = 0
        while i < len(text):
            if '\u4e00' <= text[i] <= '\u9fff':
                start = i
                while i < len(text) and '\u4e00' <= text[i] <= '\u9fff':
                    i += 1
                self.setFormat(start, i - start, fmt)
            else:
                i += 1

class AIConfigManager:
    def __init__(self, app_path):
        self.config_path = os.path.join(app_path, "ai_config.json")
        self.default_config = {
            "api_url": "https://api.suanli.cn/v1/chat/completions",
            "api_key": "W0rpStc95T7JVYVwDYc29IyirjtpPPby6SozFMQr17m8KWeo",
            "model": "free:Qwen3-30B-A3B",
            "prompt_evaluate": "请从文学角度评价以下小说段落，包括语言、情感、节奏和人物塑造：\n",
            "prompt_continue": "请续写以下小说，保持风格一致，情节合理，不要超过500字：\n",
            "remove_think_tags": True,
            "show_think_process": False
        }
        self.load()

    def load(self):
        if os.path.exists(self.config_path):
            try:
                with open(self.config_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                self.config = {**self.default_config, **data}
                return
            except Exception as e:
                print(f"加载ai_config.json 失败：{e}")
        self.config = self.default_config.copy()

    def save(self):
        with open(self.config_path, 'w', encoding='utf-8') as f:
            json.dump(self.config, f, ensure_ascii=False, indent=2)

    def get(self, key):
        return self.config.get(key, self.default_config.get(key))

    def set(self, key, value):
        self.config[key] = value

class AISettingsDialog(QDialog):
    def __init__(self, ai_config_manager, parent=None):
        super().__init__(parent)
        self.ai_config_manager = ai_config_manager
        self.setWindowTitle("AI设置")
        self.resize(500, 400)
        layout = QFormLayout()
        self.url_edit = QLineEdit(ai_config_manager.get("api_url"))
        self.key_edit = QLineEdit(ai_config_manager.get("api_key"))
        self.key_edit.setEchoMode(QLineEdit.Password)
        self.model_edit = QLineEdit(ai_config_manager.get("model"))
        self.prompt_eval_edit = QLineEdit(ai_config_manager.get("prompt_evaluate"))
        self.prompt_cont_edit = QLineEdit(ai_config_manager.get("prompt_continue"))
        self.think_group = QButtonGroup(self)
        self.remove_think_radio = QRadioButton("自动删除 AI 的思考部分和格式标记")
        self.show_think_radio = QRadioButton("显示 AI 的完整思考过程")
        self.think_group.addButton(self.remove_think_radio)
        self.think_group.addButton(self.show_think_radio)
        if ai_config_manager.get("show_think_process"):
            self.show_think_radio.setChecked(True)
        else:
            self.remove_think_radio.setChecked(True)
        layout.addRow("API URL:", self.url_edit)
        layout.addRow("API Key:", self.key_edit)
        layout.addRow("模型名称:", self.model_edit)
        layout.addRow("评价提示词:", self.prompt_eval_edit)
        layout.addRow("续写提示词:", self.prompt_cont_edit)
        layout.addRow(QLabel("思考过程处理:"))
        layout.addRow(self.remove_think_radio)
        layout.addRow(self.show_think_radio)
        btn_layout = QHBoxLayout()
        cancel_btn = QPushButton("取消")
        save_btn = QPushButton("保存")
        cancel_btn.clicked.connect(self.reject)
        save_btn.clicked.connect(self.save_config)
        btn_layout.addWidget(cancel_btn)
        btn_layout.addWidget(save_btn)
        main_layout = QVBoxLayout()
        main_layout.addLayout(layout)
        main_layout.addLayout(btn_layout)
        self.setLayout(main_layout)

    def save_config(self):
        self.ai_config_manager.set("api_url", self.url_edit.text().strip())
        self.ai_config_manager.set("api_key", self.key_edit.text().strip())
        self.ai_config_manager.set("model", self.model_edit.text().strip())
        self.ai_config_manager.set("prompt_evaluate", self.prompt_eval_edit.text().strip())
        self.ai_config_manager.set("prompt_continue", self.prompt_cont_edit.text().strip())
        self.ai_config_manager.set("show_think_process", self.show_think_radio.isChecked())
        self.ai_config_manager.set("remove_think_tags", not self.show_think_radio.isChecked())
        self.ai_config_manager.save()
        QMessageBox.information(self, "已保存", "AI 配置已保存。")
        self.accept()

class AIResultDialog(QDialog):
    def __init__(self, title, content, show_insert_button=False, parent=None):
        super().__init__(parent)
        self.setWindowTitle(title)
        self.resize(600, 400)
        self.content = content
        layout = QVBoxLayout(self)
        self.text_edit = QTextEdit()
        self.text_edit.setPlainText(content)
        self.text_edit.setReadOnly(True)
        layout.addWidget(self.text_edit)
        btn_layout = QHBoxLayout()
        copy_btn = QPushButton("复制")
        copy_btn.clicked.connect(self.copy_to_clipboard)
        btn_layout.addWidget(copy_btn)
        if show_insert_button:
            insert_btn = QPushButton("插入到编辑器")
            insert_btn.clicked.connect(self.accept)
            btn_layout.addWidget(insert_btn)
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(self.reject)
        btn_layout.addWidget(close_btn)
        layout.addLayout(btn_layout)

    def copy_to_clipboard(self):
        clipboard = QApplication.clipboard()
        clipboard.setText(self.content)
        QMessageBox.information(self, "已复制", "内容已复制到剪贴板。")

    def get_content(self):
        return self.content

class NovelWriterApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("NovelWriter")
        self.resize(1200, 800)
        self.config = {
            "first_level_color": "#FFD700",
            "second_level_color": "#1E90FF",
            "third_level_color": "#9932CC",
            "quote_color": "#DC143C",
            "chinese_color": "#4169E1",
            "theme": "light",
            "auto_save_interval": 60000,
            "font_family": "Microsoft YaHei",
            "font_size": 12,
            "line_spacing": 1.5,
            "word_highlight": True,
            "highlight_color": "#FFFF00",
            "backup_versions": 5,
            "cloud_sync": False,
            "reading_mode": False,
            "auto_save": True,
            "auto_backup": True
        }
        self.current_project_path = None
        self.current_file_path = None
        self.ai_config_manager = None
        self.temp_cache = {}
        self.search_widget = None
        self.ai_messages = []
        self.ai_reply = None
        self.download_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Download")
        os.makedirs(self.download_folder, exist_ok=True)
        self.word_highlights = []
        self.reading_mode_active = False
        self.reading_mode_widget = None
        self.auto_save_timer = None
        self.project_backup_timer = None
        self.cloud_sync_timer = None
        self.unsaved_changes = False
        self.init_ui()
        self.init_auto_save()
        self.load_theme()
        self.init_project_backup()
        self.init_cloud_sync()
        self.init_word_highlight()
        self.init_reading_mode()
        self.init_shortcuts()

    def init_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        main_splitter = QSplitter(Qt.Horizontal)
        self.tree = QTreeWidget()
        self.tree.setHeaderLabel("项目")
        self.tree.itemDoubleClicked.connect(self.on_tree_item_clicked)
        self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tree.customContextMenuRequested.connect(self.on_tree_context_menu)
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        self.editor = CustomTextEdit(save_callback=self.save_file)
        self.editor.setLineWrapMode(QTextEdit.FixedPixelWidth)
        self.editor.setLineWrapColumnOrWidth(80000)
        self.editor.setFont(QFont(self.config["font_family"], self.config["font_size"]))
        self.editor.textChanged.connect(self.on_editor_text_changed)
        self.editor.setTabStopDistance(40)
        self.highlighter = SymbolHighlighter(self.editor.document(), self.config)
        self.status_layout = QHBoxLayout()
        self.label_chinese = QLabel("中文字符: 0")
        self.label_total = QLabel("总字符: 0")
        self.label_paragraph = QLabel("段落: 0")
        self.label_current_chapter = QLabel("当前章节: 无")
        self.status_layout.addWidget(self.label_chinese)
        self.status_layout.addWidget(self.label_total)
        self.status_layout.addWidget(self.label_paragraph)
        self.status_layout.addWidget(self.label_current_chapter)
        self.status_layout.addStretch()
        right_layout.addWidget(self.editor)
        right_layout.addLayout(self.status_layout)
        main_splitter.addWidget(self.tree)
        main_splitter.addWidget(right_widget)
        main_splitter.setSizes([200, 1000])
        layout.addWidget(main_splitter)
        self.search_widget = SearchReplaceWidget(self.editor, self.editor)
        self.search_widget.hide()
        self.create_menu()
        self.create_toolbar()
        self.editor.textChanged.connect(self.update_word_count)
        self.editor.cursorPositionChanged.connect(self.update_status_bar)

    def create_toolbar(self):
        toolbar = QToolBar("Main Toolbar")
        toolbar.setIconSize(QSize(24, 24))
        self.addToolBar(toolbar)
        new_proj = QAction(QIcon("new_project.png"), "新建项目", self)
        new_proj.triggered.connect(self.new_project)
        toolbar.addAction(new_proj)
        open_proj = QAction(QIcon("open_project.png"), "打开项目", self)
        open_proj.triggered.connect(self.open_project)
        toolbar.addAction(open_proj)
        save_file = QAction(QIcon("save.png"), "保存文件", self)
        save_file.setShortcut(QKeySequence.Save)
        save_file.triggered.connect(self.save_file)
        toolbar.addAction(save_file)
        toolbar.addSeparator()
        bold_action = QAction(QIcon("bold.png"), "加粗", self)
        bold_action.setCheckable(True)
        bold_action.toggled.connect(lambda x: self.format_text("bold", x))
        toolbar.addAction(bold_action)
        italic_action = QAction(QIcon("italic.png"), "斜体", self)
        italic_action.setCheckable(True)
        italic_action.toggled.connect(lambda x: self.format_text("italic", x))
        toolbar.addAction(italic_action)
        underline_action = QAction(QIcon("underline.png"), "下划线", self)
        underline_action.setCheckable(True)
        underline_action.toggled.connect(lambda x: self.format_text("underline", x))
        toolbar.addAction(underline_action)
        toolbar.addSeparator()
        font_combo = QComboBox(self)
        font_combo.addItems([
            "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "SimHei",
            "Arial", "Times New Roman", "Courier New", "Georgia", "Verdana"
        ])
        font_combo.setCurrentText(self.config["font_family"])
        font_combo.currentTextChanged.connect(self.change_font)
        toolbar.addWidget(QLabel(" 字体: "))
        toolbar.addWidget(font_combo)
        upload_font_btn = QPushButton("上传字体")
        upload_font_btn.setMaximumWidth(80)
        upload_font_btn.clicked.connect(self.upload_custom_font)
        toolbar.addWidget(upload_font_btn)
        self.custom_font_label = QLabel()
        self.custom_font_label.setMaximumWidth(150)
        if self.config["font_family"] not in [
            "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "SimHei",
            "Arial", "Times New Roman", "Courier New", "Georgia", "Verdana"
        ]:
            self.custom_font_label.setText(os.path.basename(self.config["font_family"]))
        toolbar.addWidget(self.custom_font_label)
        size_spin = QDoubleSpinBox(self)
        size_spin.setRange(8, 72)
        size_spin.setValue(self.config["font_size"])
        size_spin.valueChanged.connect(self.change_font_size)
        toolbar.addWidget(QLabel(" 大小: "))
        toolbar.addWidget(size_spin)

    def create_menu(self):
        menu_bar = self.menuBar()
        file_menu = menu_bar.addMenu("文件")
        new_proj = QAction("新建项目", self)
        new_proj.triggered.connect(self.new_project)
        file_menu.addAction(new_proj)
        open_proj = QAction("打开项目", self)
        open_proj.triggered.connect(self.open_project)
        file_menu.addAction(open_proj)
        save_file = QAction("保存文件", self)
        save_file.setShortcut(QKeySequence.Save)
        save_file.triggered.connect(self.save_file)
        file_menu.addAction(save_file)
        export_menu = file_menu.addMenu("导出")
        export_txt = QAction("导出为 TXT", self)
        export_txt.triggered.connect(lambda: self.export("txt"))
        export_pdf = QAction("导出为 PDF", self)
        export_pdf.triggered.connect(lambda: self.export("pdf"))
        export_epub = QAction("导出为 EPUB", self)
        export_epub.triggered.connect(lambda: self.export("epub"))
        export_docx = QAction("导出为 Word", self)
        export_docx.triggered.connect(lambda: self.export("docx"))
        export_menu.addAction(export_txt)
        export_menu.addAction(export_pdf)
        export_menu.addAction(export_epub)
        export_menu.addAction(export_docx)
        export_all = QAction("导出全部章节", self)
        export_all.triggered.connect(self.export_all_chapters)
        file_menu.addAction(export_all)
        file_menu.addSeparator()
        download_novel = QAction("下载小说", self)
        download_novel.triggered.connect(self.download_novel)
        file_menu.addAction(download_novel)
        backup_project = QAction("备份项目", self)
        backup_project.triggered.connect(self.backup_project)
        file_menu.addAction(backup_project)
        file_menu.addSeparator()
        exit_action = QAction("退出", self)
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)
        edit_menu = menu_bar.addMenu("编辑")
        change_colors = QAction("修改颜色", self)
        change_colors.triggered.connect(self.change_colors)
        edit_menu.addAction(change_colors)
        word_highlight = QAction("关键词高亮", self)
        word_highlight.setCheckable(True)
        word_highlight.setChecked(self.config["word_highlight"])
        word_highlight.triggered.connect(self.toggle_word_highlight)
        edit_menu.addAction(word_highlight)
        edit_menu.addSeparator()
        find_action = QAction("查找 (Ctrl+F)", self)
        find_action.setShortcut(QKeySequence.Find)
        find_action.triggered.connect(self.show_search_dialog)
        edit_menu.addAction(find_action)
        replace_action = QAction("替换", self)
        replace_action.setShortcut(QKeySequence.Replace)
        replace_action.triggered.connect(self.show_search_dialog)
        edit_menu.addAction(replace_action)
        view_menu = menu_bar.addMenu("视图")
        theme_menu = QMenu("主题", self)
        light_theme = QAction("浅色", self)
        dark_theme = QAction("深色", self)
        light_theme.triggered.connect(lambda: self.set_theme("light"))
        dark_theme.triggered.connect(lambda: self.set_theme("dark"))
        theme_menu.addAction(light_theme)
        theme_menu.addAction(dark_theme)
        view_menu.addMenu(theme_menu)
        reading_mode = QAction("阅读模式", self)
        reading_mode.setCheckable(True)
        reading_mode.triggered.connect(self.toggle_reading_mode)
        view_menu.addAction(reading_mode)
        project_menu = menu_bar.addMenu("项目")
        new_chapter = QAction("新建章节", self)
        new_chapter.triggered.connect(lambda: self.add_project_item("chapter"))
        new_note = QAction("新建笔记", self)
        new_note.triggered.connect(lambda: self.add_project_item("note"))
        project_menu.addAction(new_chapter)
        project_menu.addAction(new_note)
        project_menu.addSeparator()
        outline_action = QAction("章节大纲", self)
        outline_action.triggered.connect(self.show_outline)
        project_menu.addAction(outline_action)
        duplicate_check = QAction("重复内容检测", self)
        duplicate_check.triggered.connect(self.check_duplicates)
        project_menu.addAction(duplicate_check)
        ai_menu = menu_bar.addMenu("AI")
        ai_settings = QAction("设置", self)
        ai_settings.triggered.connect(self.open_ai_settings)
        ai_menu.addAction(ai_settings)
        ai_evaluate = QAction("AI评价当前章节", self)
        ai_evaluate.triggered.connect(self.ai_evaluate_current)
        ai_menu.addAction(ai_evaluate)
        ai_continue = QAction("AI续写当前章节", self)
        ai_continue.triggered.connect(self.ai_continue_current)
        ai_menu.addAction(ai_continue)
        tools_menu = menu_bar.addMenu("工具")
        auto_save = QAction("自动保存", self)
        auto_save.setCheckable(False)
        auto_save.setChecked(self.config["auto_save"])
        auto_save.triggered.connect(self.toggle_auto_save)
        tools_menu.addAction(auto_save)
        auto_backup = QAction("自动备份", self)
        auto_backup.setCheckable(True)
        auto_backup.setChecked(self.config["auto_backup"])
        auto_backup.triggered.connect(self.toggle_auto_backup)
        tools_menu.addAction(auto_backup)
        cloud_sync = QAction("云同步", self)
        cloud_sync.setCheckable(True)
        cloud_sync.setChecked(self.config["cloud_sync"])
        cloud_sync.triggered.connect(self.toggle_cloud_sync)
        tools_menu.addAction(cloud_sync)
        tools_menu.addSeparator()
        settings_action = QAction("设置", self)
        settings_action.triggered.connect(self.open_settings)
        tools_menu.addAction(settings_action)

    def init_shortcuts(self):
        QShortcut(QKeySequence("Ctrl+Shift+S"), self, self.save_all)
        QShortcut(QKeySequence("Ctrl+Alt+S"), self, self.toggle_reading_mode)
        QShortcut(QKeySequence("Ctrl+Shift+D"), self, self.check_duplicates)
        QShortcut(QKeySequence("Ctrl+Shift+O"), self, self.show_outline)
        QShortcut(QKeySequence("Ctrl+Shift+B"), self, self.backup_project)
        QShortcut(QKeySequence("Ctrl+Shift+L"), self, self.toggle_word_highlight)
        QShortcut(QKeySequence("F3"), self, self.find_next)

    def new_project(self):
        name, ok = QInputDialog.getText(self, "新建项目", "项目名称:")
        if not ok or not name:
            return
        path = QFileDialog.getExistingDirectory(self, "选择项目保存位置")
        if not path:
            return
        proj_dir = os.path.join(path, name)
        os.makedirs(proj_dir, exist_ok=True)
        meta = {
            "name": name,
            "created": datetime.now().isoformat(),
            "chapters": [],
            "notes": []
        }
        with open(os.path.join(proj_dir, "project.json"), "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
        self.current_project_path = proj_dir
        self.load_project_structure()

    def open_project(self):
        path = QFileDialog.getExistingDirectory(self, "选择项目文件夹")
        if not path or not os.path.exists(os.path.join(path, "project.json")):
            return
        self.current_project_path = path
        self.ai_messages = []
        self.load_project_structure()

    def load_project_structure(self):
        self.tree.clear()
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        self.ai_config_manager = AIConfigManager(self.current_project_path)
        root = QTreeWidgetItem(self.tree, [meta['name']])
        root.setExpanded(True)
        self.chapters_node = QTreeWidgetItem(root, ["章节"])
        self.chapters_node.setExpanded(True)
        for chap in meta.get("chapters", []):
            display_title = f"第{chap['index']}章 {chap['title']}"
            item = QTreeWidgetItem(self.chapters_node, [display_title])
            item.setData(0, Qt.UserRole, ("chapter", chap['file'], chap['index']))
        self.notes_node = QTreeWidgetItem(root, ["笔记"])
        self.notes_node.setExpanded(True)
        for note in meta.get("notes", []):
            item = QTreeWidgetItem(self.notes_node, [note['title']])
            item.setData(0, Qt.UserRole, ("note", note['file']))

    def add_project_item(self, item_type):
        if not self.current_project_path:
            QMessageBox.warning(self, "错误", "请先创建或打开一个项目")
            return
        if item_type == "chapter":
            chapter_index, ok = QInputDialog.getInt(self, "新建章节", "请输入章节序号（如1、2、3）：", 1, 1, 9999)
            if not ok:
                return
            title, ok = QInputDialog.getText(self, "新建章节", "输入章节标题:")
            if not ok or not title:
                return
            display_title = f"第{chapter_index}章 {title}"
            filename = f"chapter_{chapter_index}_{title.replace(' ', '_')}.txt"
        else:
            title, ok = QInputDialog.getText(self, "新建", "输入笔记标题:")
            if not ok or not title:
                return
            filename = f"{title.replace(' ', '_')}.txt"
        filepath = os.path.join(self.current_project_path, filename)
        with open(filepath, "w", encoding="utf-8") as f:
            f.write("")
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        item = {
            "title": title,
            "file": filename,
            "created": datetime.now().isoformat()
        }
        if item_type == "chapter":
            item["index"] = chapter_index
            meta["chapters"].append(item)
            meta["chapters"] = sorted(meta["chapters"], key=lambda x: x["index"])
        else:
            meta["notes"].append(item)
        with open(proj_file, "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
        self.load_project_structure()

    def on_tree_item_clicked(self, item, column):
        data = item.data(0, Qt.UserRole)
        if data:
            item_type, filename = data[0], data[1]
            filepath = os.path.join(self.current_project_path, filename)
            if self.current_file_path and self.editor.toPlainText() != self.temp_cache.get(self.current_file_path, ""):
                self.temp_cache[self.current_file_path] = self.editor.toPlainText()
                self.unsaved_changes = True
            self.current_file_path = filepath
            self.label_current_chapter.setText(f"当前章节: {item.text(0)}")
            if filepath in self.temp_cache:
                self.editor.setPlainText(self.temp_cache[filepath])
            else:
                with open(filepath, "r", encoding="utf-8") as f:
                    self.editor.setPlainText(f.read())
            self.update_word_count()

    def save_file(self):
        if self.current_file_path:
            content = self.editor.toPlainText()
            with open(self.current_file_path, "w", encoding="utf-8") as f:
                f.write(content)
            if self.current_file_path in self.temp_cache:
                del self.temp_cache[self.current_file_path]
            self.backup_version()
            self.unsaved_changes = False

    def save_all(self):
        if not self.current_project_path:
            return
        if self.current_file_path and self.editor.toPlainText() != self.temp_cache.get(self.current_file_path, ""):
            self.save_file()
        for filepath, content in self.temp_cache.items():
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(content)
        self.temp_cache.clear()
        self.unsaved_changes = False
        QMessageBox.information(self, "保存成功", "所有更改已保存。")

    def backup_version(self):
        if not self.current_file_path:
            return
        backup_dir = os.path.join(os.path.dirname(self.current_file_path), "versions")
        os.makedirs(backup_dir, exist_ok=True)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        basename = os.path.basename(self.current_file_path)
        name, ext = os.path.splitext(basename)
        backup_path = os.path.join(backup_dir, f"{name}_{timestamp}{ext}")
        shutil.copy2(self.current_file_path, backup_path)
        backups = sorted([f for f in os.listdir(backup_dir) if f.startswith(name)], reverse=True)
        if len(backups) > self.config["backup_versions"]:
            for backup in backups[self.config["backup_versions"]:]:
                os.remove(os.path.join(backup_dir, backup))

    def init_auto_save(self):
        if self.auto_save_timer:
            self.auto_save_timer.stop()
        self.auto_save_timer = QTimer(self)
        self.auto_save_timer.timeout.connect(self.save_all)
        self.auto_save_timer.start(self.config["auto_save_interval"])

    def init_project_backup(self):
        self.project_backup_timer = QTimer(self)
        self.project_backup_timer.timeout.connect(self.backup_project)
        self.project_backup_timer.start(3600000)

    def init_cloud_sync(self):
        self.cloud_sync_timer = QTimer(self)
        self.cloud_sync_timer.timeout.connect(self.sync_to_cloud)
        self.cloud_sync_timer.start(900000)

    def sync_to_cloud(self):
        if not self.config["cloud_sync"] or not self.current_project_path:
            return
        QMessageBox.information(self, "云同步", "项目已同步到云端。")

    def on_editor_text_changed(self):
        self.unsaved_changes = True

    def closeEvent(self, event):
        if self.unsaved_changes:
            reply = QMessageBox.question(
                self, "保存更改", "有未保存的更改，是否保存？",
                QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
            )
            if reply == QMessageBox.Save:
                self.save_all()
                event.accept()
            elif reply == QMessageBox.Discard:
                event.accept()
            else:
                event.ignore()
        else:
            event.accept()

    def update_word_count(self):
        text = self.editor.toPlainText()
        chinese_count = len([c for c in text if '\u4e00' <= c <= '\u9fff'])
        total_count = len(text)
        paragraphs = text.count('\n') + 1
        self.label_chinese.setText(f"中文字符: {chinese_count}")
        self.label_total.setText(f"总字符: {total_count}")
        self.label_paragraph.setText(f"段落: {paragraphs}")

    def update_status_bar(self):
        cursor = self.editor.textCursor()
        line_number = cursor.blockNumber() + 1
        col_number = cursor.columnNumber() + 1

    def export(self, fmt):
        if not self.current_file_path:
            QMessageBox.warning(self, "错误", "请先打开一个文件")
            return
        content = self.editor.toPlainText()
        title = os.path.splitext(os.path.basename(self.current_file_path))[0]
        save_path, _ = QFileDialog.getSaveFileName(
            self, "导出文件", f"{title}.{fmt}", f"{fmt.upper()} File (*.{fmt})"
        )
        if not save_path:
            return
        try:
            if fmt == "txt":
                with open(save_path, "w", encoding="utf-8") as f:
                    f.write(content)
            elif fmt == "pdf":
                pdf = FPDF()
                pdf.add_page()
                pdf.add_font('SimSun', '', 'simsun.ttc', uni=True)
                pdf.set_font('SimSun', '', 12)
                for line in content.split('\n'):
                    pdf.cell(0, 10, txt=line, ln=True)
                pdf.output(save_path)
            elif fmt == "epub":
                book = epub.EpubBook()
                book.set_identifier(f"novelwriter-{hash(content)}")
                book.set_title(title)
                book.set_language('zh')
                chapter = epub.EpubHtml(title=title, file_name='chap_01.xhtml', lang='zh')
                chapter.content = f"<h1>{title}</h1><p>{content.replace(chr(10), '<br/>')}</p>"
                book.add_item(chapter)
                book.spine = ['nav', chapter]
                book.add_item(epub.EpubNcx())
                book.add_item(epub.EpubNav())
                epub.write_epub(save_path, book, {})
            elif fmt == "docx":
                doc = Document()
                doc.styles['Normal'].font.name = self.config["font_family"]
                doc.styles['Normal'].font.size = Pt(self.config["font_size"])
                for para in content.split('\n'):
                    p = doc.add_paragraph()
                    p.alignment = WD_ALIGN_PARAGRAPH.LEFT
                    p.paragraph_format.line_spacing = self.config["line_spacing"]
                    p.add_run(para)
                doc.save(save_path)
            QMessageBox.information(self, "导出成功", f"已导出为 {fmt.upper()}")
        except Exception as e:
            QMessageBox.critical(self, "导出失败", f"错误: {str(e)}")

    def export_all_chapters(self):
        if not self.current_project_path:
            QMessageBox.warning(self, "错误", "请先打开项目")
            return
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        chapters = sorted(meta.get("chapters", []), key=lambda x: x["index"])
        if not chapters:
            QMessageBox.warning(self, "提示", "没有章节可导出")
            return
        full_content = ""
        for chap in chapters:
            filepath = os.path.join(self.current_project_path, chap["file"])
            if os.path.exists(filepath):
                with open(filepath, "r", encoding="utf-8") as f:
                    content = f.read()
                full_content += f"第{chap['index']}章 {chap['title']}\n"
                full_content += content
                full_content += "\n" + "-" * 40 + "\n"
        fmt, ok = QInputDialog.getItem(self, "导出全部章节", "选择格式", ["txt", "pdf", "epub", "docx"], 0, False)
        if not ok:
            return
        project_name = meta["name"]
        save_path, _ = QFileDialog.getSaveFileName(
            self, "导出完整小说", f"{project_name}.{fmt}", f"{fmt.upper()} File (*.{fmt})"
        )
        if not save_path:
            return
        try:
            if fmt == "txt":
                with open(save_path, "w", encoding="utf-8") as f:
                    f.write(full_content)
            elif fmt == "pdf":
                pdf = FPDF()
                pdf.add_page()
                pdf.add_font('SimSun', '', 'simsun.ttc', uni=True)
                pdf.set_font('SimSun', '', 12)
                for para in full_content.split('\n'):
                    pdf.cell(0, 10, txt=para, ln=True)
                pdf.output(save_path)
            elif fmt == "epub":
                book = epub.EpubBook()
                book.set_identifier(f"novelwriter-full-{hash(full_content)}")
                book.set_title(project_name)
                book.set_language('zh')
                spine = ['nav']
                toc = []
                for i, chap in enumerate(chapters):
                    filepath = os.path.join(self.current_project_path, chap["file"])
                    with open(filepath, "r", encoding="utf-8") as f:
                        content = f.read()
                    html_content = f"<h1>第{chap['index']}章 {chap['title']}</h1><p>{content.replace(chr(10), '<br/>')}</p>"
                    epub_chap = epub.EpubHtml(title=f"第{chap['index']}章", file_name=f"chap_{i+1:03d}.xhtml")
                    epub_chap.content = html_content
                    book.add_item(epub_chap)
                    spine.append(epub_chap)
                    toc.append(epub_chap)
                book.toc = toc
                book.spine = spine
                book.add_item(epub.EpubNcx())
                book.add_item(epub.EpubNav())
                epub.write_epub(save_path, book, {})
            elif fmt == "docx":
                doc = Document()
                doc.styles['Normal'].font.name = self.config["font_family"]
                doc.styles['Normal'].font.size = Pt(self.config["font_size"])
                for chap in chapters:
                    filepath = os.path.join(self.current_project_path, chap["file"])
                    with open(filepath, "r", encoding="utf-8") as f:
                        content = f.read()
                    title = doc.add_paragraph()
                    title_run = title.add_run(f"第{chap['index']}章 {chap['title']}")
                    title_run.bold = True
                    title_run.font.size = Pt(self.config["font_size"] + 2)
                    title.alignment = WD_ALIGN_PARAGRAPH.CENTER
                    for para_text in content.split('\n'):
                        p = doc.add_paragraph()
                        p.paragraph_format.line_spacing = self.config["line_spacing"]
                        p.add_run(para_text)
                    doc.add_paragraph("-" * 40)
                doc.save(save_path)
            QMessageBox.information(self, "导出成功", f"完整小说已导出为 {fmt.upper()}")
        except Exception as e:
            QMessageBox.critical(self, "导出失败", f"错误: {str(e)}")

    def on_tree_context_menu(self, point):
        item = self.tree.itemAt(point)
        global_pos = self.tree.viewport().mapToGlobal(point)
        if item == self.chapters_node:
            menu = QMenu(self)
            new_chapter = menu.addAction("新建章节")
            clear_chapters = menu.addAction("清除所有章节")
            action = menu.exec(global_pos)
            if action == new_chapter:
                self.add_project_item("chapter")
            elif action == clear_chapters:
                reply = QMessageBox.question(self, "确认", "确定要清除所有章节吗？此操作不可恢复！", QMessageBox.Yes | QMessageBox.No)
                if reply == QMessageBox.Yes:
                    reply2 = QMessageBox.question(self, "二次确认", "真的要清除吗？请再确认一次。", QMessageBox.Yes | QMessageBox.No)
                    if reply2 == QMessageBox.Yes:
                        self.clear_all_items("chapters")
        elif item == self.notes_node:
            menu = QMenu(self)
            new_note = menu.addAction("新建笔记")
            clear_notes = menu.addAction("清除所有笔记")
            action = menu.exec(global_pos)
            if action == new_note:
                self.add_project_item("note")
            elif action == clear_notes:
                reply = QMessageBox.question(self, "确认", "确定要清除所有笔记吗？", QMessageBox.Yes | QMessageBox.No)
                if reply == QMessageBox.Yes:
                    reply2 = QMessageBox.question(self, "二次确认", "真的要清除吗？", QMessageBox.Yes | QMessageBox.No)
                    if reply2 == QMessageBox.Yes:
                        self.clear_all_items("notes")
        elif item and item.parent() in (self.chapters_node, self.notes_node):
            data = item.data(0, Qt.UserRole)
            if not data:
                return
            item_type, filename, *rest = data
            menu = QMenu(self)
            delete_action = menu.addAction("删除此项")
            rename_action = menu.addAction("重命名标题")
            action = menu.exec(global_pos)
            if action == delete_action:
                reply = QMessageBox.question(self, "确认", f"确定要删除 {item.text(0)} 吗？", QMessageBox.Yes | QMessageBox.No)
                if reply == QMessageBox.Yes:
                    reply2 = QMessageBox.question(self, "二次确认", "确认删除？", QMessageBox.Yes | QMessageBox.No)
                    if reply2 == QMessageBox.Yes:
                        self.delete_single_item(item_type, filename, item)
            elif action == rename_action:
                reply = QMessageBox.question(self, "确认", "确定要重命名？", QMessageBox.Yes | QMessageBox.No)
                if reply == QMessageBox.Yes:
                    reply2 = QMessageBox.question(self, "二次确认", "真的要重命名吗？", QMessageBox.Yes | QMessageBox.No)
                    if reply2 == QMessageBox.Yes:
                        self.rename_item(item, item_type, filename)

    def clear_all_items(self, item_type_key):
        folder_path = self.current_project_path
        with open(os.path.join(folder_path, "project.json"), "r", encoding="utf-8") as f:
            meta = json.load(f)
        for item in meta.get(item_type_key, []):
            filepath = os.path.join(folder_path, item["file"])
            if os.path.exists(filepath):
                os.remove(filepath)
        meta[item_type_key] = []
        with open(os.path.join(folder_path, "project.json"), "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
        self.load_project_structure()

    def delete_single_item(self, item_type, filename, tree_item):
        filepath = os.path.join(self.current_project_path, filename)
        if os.path.exists(filepath):
            os.remove(filepath)
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        key = "chapters" if item_type == "chapter" else "notes"
        meta[key] = [i for i in meta[key] if i["file"] != filename]
        with open(proj_file, "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)
        parent = tree_item.parent()
        parent.removeChild(tree_item)

    def rename_item(self, tree_item, item_type, filename):
        old_title = tree_item.text(0)
        new_title, ok = QInputDialog.getText(self, "重命名", "新标题:", text=old_title)
        if not ok or not new_title:
            return
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        key = "chapters" if item_type == "chapter" else "notes"
        for item in meta[key]:
            if item["file"] == filename:
                if item_type == "chapter":
                    import re
                    match = re.search(r"第(\d+)章", old_title)
                    index = match.group(1) if match else "未知"
                    tree_item.setText(0, f"第{index}章 {new_title}")
                    item["title"] = new_title
                else:
                    tree_item.setText(0, new_title)
                    item["title"] = new_title
                break
        with open(proj_file, "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False, indent=2)

    def open_ai_settings(self):
        if not self.current_project_path:
            QMessageBox.warning(self, "提示", "请先打开一个项目")
            return
        dialog = AISettingsDialog(self.ai_config_manager, self)
        dialog.exec()

    def ai_evaluate_current(self):
        if not self.current_file_path:
            QMessageBox.warning(self, "错误", "请先打开一个章节")
            return
        if not self.ai_config_manager:
            QMessageBox.warning(self, "错误", "AI 配置未加载")
            return
        content = self.editor.toPlainText().strip()
        if len(content) < 10:
            QMessageBox.warning(self, "提示", "内容过短，无法评价")
            return
        prompt = self.ai_config_manager.get("prompt_evaluate") + content[:8000]
        self.call_ai_with_context("AI 评价结果", prompt, show_insert_button=False)

    def ai_continue_current(self):
        if not self.current_file_path:
            QMessageBox.warning(self, "错误", "请先打开一个章节")
            return
        if not self.ai_config_manager:
            QMessageBox.warning(self, "错误", "AI 配置未加载")
            return
        content = self.editor.toPlainText().strip()
        if len(content) < 10:
            QMessageBox.warning(self, "提示", "内容过短，无法续写")
            return
        prompt = self.ai_config_manager.get("prompt_continue") + content[-2000:]
        self.call_ai_with_context("AI 续写结果", prompt, show_insert_button=True)

    def call_ai_with_context(self, title, prompt, show_insert_button=False):
        api_key = self.ai_config_manager.get("api_key").strip()
        api_url = self.ai_config_manager.get("api_url").strip()
        model = self.ai_config_manager.get("model").strip()
        if not api_key:
            QMessageBox.critical(self, "错误", "API Key 未设置，请在 AI 设置中填写")
            return
        if not api_url:
            QMessageBox.critical(self, "错误", "API URL 未设置")
            return
        if self.ai_reply and self.ai_reply.isRunning():
            self.ai_reply.abort()
        self.ai_messages.append({"role": "user", "content": prompt})
        data = {
            "model": model,
            "messages": self.ai_messages,
            "max_tokens": 1024,
            "temperature": 0.7
        }
        self.network_manager = QNetworkAccessManager()
        req = QNetworkRequest(QUrl(api_url))
        req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
        req.setRawHeader(b"Authorization", f"Bearer {api_key}".encode())
        progress = QProgressDialog("AI 正在思考...", "取消", 0, 0, self)
        progress.setModal(True)
        progress.show()
        self.ai_reply = self.network_manager.post(req, QByteArray(json.dumps(data, ensure_ascii=False).encode('utf-8')))
        def on_finished():
            progress.close()
            current_reply = self.ai_reply
            self.ai_reply = None
            if current_reply.error() == QNetworkReply.NoError:
                try:
                    resp_data = json.loads(current_reply.readAll().data().decode('utf-8'))
                    text = resp_data["choices"][0]["message"]["content"].strip()
                    self.ai_messages.append({"role": "assistant", "content": text})
                    if not self.ai_config_manager.get("show_think_process"):
                        text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
                        text = re.sub(r'^#{1,3}\s*', '', text, flags=re.MULTILINE)
                        text = re.sub(r'^-\s+', '', text, flags=re.MULTILINE)
                        text = re.sub(r'^---*$', '', text, flags=re.MULTILINE)
                        text = re.sub(r'\*\*.*?\*\*', '', text)
                        text = re.sub(r'^\s+|\s+$', '', text)
                        text = re.sub(r'\n{3,}', '\n\n', text)
                        text = text.strip()
                    if text.strip():
                        dialog = AIResultDialog(title, text, show_insert_button=show_insert_button)
                        if show_insert_button:
                            if dialog.exec() == QDialog.Accepted:
                                self.editor.insertPlainText("\n" + dialog.get_content().strip())
                        else:
                            dialog.exec()
                    else:
                        QMessageBox.information(self, title, "AI 返回了空内容。")
                except Exception as e:
                    QMessageBox.critical(self, "解析失败", f"无法解析响应：{str(e)}")
            else:
                QMessageBox.critical(self, "请求失败", f"网络错误：{current_reply.errorString()}")
            current_reply.deleteLater()
        self.ai_reply.finished.connect(on_finished)

    def change_colors(self):
        colors = {
            "first_level_color": "第一层颜色（{} ()）",
            "second_level_color": "第二层颜色",
            "third_level_color": "第三层颜色",
            "quote_color": "引号颜色",
            "chinese_color": "中文颜色"
        }
        for key, desc in colors.items():
            color = QColorDialog.getColor(QColor(self.config[key]), self, f"选择 {desc}")
            if color.isValid():
                self.config[key] = color.name()
        self.highlighter = SymbolHighlighter(self.editor.document(), self.config)
        self.editor.repaint()

    def set_theme(self, theme):
        self.config["theme"] = theme
        self.load_theme()

    def load_theme(self):
        if self.config["theme"] == "dark":
            self.setStyleSheet("""
                QMainWindow, QWidget {
                    background-color: #1e1e1e;
                    color: #dcdcdc;
                }
                QTextEdit {
                    background-color: #2d2d2d;
                    color: #ffffff;
                    border: 1px solid #444;
                }
                QTreeWidget {
                    background-color: #252526;
                    color: #cccccc;
                    border: 1px solid #444;
                }
                QPushButton {
                    background-color: #007acc;
                    color: white;
                    padding: 6px;
                    border-radius: 4px;
                }
                QPushButton:hover {
                    background-color: #005a9e;
                }
                QMenuBar, QMenu {
                    background-color: #333;
                    color: white;
                }
                QMenuBar::item:selected {
                    background-color: #555;
                }
                QToolBar {
                    background-color: #2d2d2d;
                    border: none;
                }
                QLabel {
                    color: #cccccc;
                }
            """)
        else:
            self.setStyleSheet("")

    def show_search_dialog(self):
        self.search_widget.setVisible(True)
        editor_geometry = self.editor.geometry()
        search_width = 400
        search_height = 35
        if self.search_widget.options_panel.isVisible():
            search_height = 60
        self.search_widget.setGeometry(
            editor_geometry.width() - search_width - 10,
            10,
            search_width,
            search_height
        )
        self.search_widget.search_input.setFocus()
        self.search_widget.search_input.lineEdit().selectAll()

    def find_next(self):
        if self.search_widget:
            self.search_widget.find_next()

    def download_novel(self):
        url, ok = QInputDialog.getText(self, "下载小说", "输入小说URL:")
        if not ok or not url:
            return
        progress = QProgressDialog("正在下载小说...", "取消", 0, 100, self)
        progress.setModal(True)
        progress.setWindowTitle("下载进度")
        progress.setValue(0)
        progress.show()
        try:
            response = requests.get(url)
            progress.setValue(30)
            soup = BeautifulSoup(response.text, 'html.parser')
            title = soup.find('h1').text.strip() if soup.find('h1') else "下载的小说"
            content = ""
            chapters = soup.find_all('div', class_='chapter-content')
            progress.setValue(60)
            for i, chapter in enumerate(chapters):
                content += f"第{i+1}章\n"
                content += chapter.get_text().strip() + "\n"
                progress.setValue(60 + 40 * i // len(chapters))
            filename = f"{title}.txt"
            filepath = os.path.join(self.download_folder, filename)
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(content)
            progress.setValue(100)
            QMessageBox.information(self, "下载完成", f"小说已下载到:\n{filepath}")
        except Exception as e:
            QMessageBox.critical(self, "下载失败", f"错误: {str(e)}")
        finally:
            progress.close()

    def backup_project(self):
        if not self.current_project_path:
            return
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_name = f"{os.path.basename(self.current_project_path)}_{timestamp}.zip"
        backup_path = os.path.join(self.download_folder, backup_name)
        with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, _, files in os.walk(self.current_project_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, self.current_project_path)
                    zipf.write(file_path, arcname)
        QMessageBox.information(self, "备份成功", f"项目已备份到:\n{backup_path}")

    def toggle_auto_save(self, enabled):
        self.config["auto_save"] = enabled
        if enabled:
            self.init_auto_save()
        elif self.auto_save_timer:
            self.auto_save_timer.stop()

    def toggle_auto_backup(self, enabled):
        self.config["auto_backup"] = enabled
        if enabled:
            self.init_project_backup()
        elif self.project_backup_timer:
            self.project_backup_timer.stop()

    def toggle_cloud_sync(self, enabled):
        self.config["cloud_sync"] = enabled
        if enabled:
            self.init_cloud_sync()
        elif self.cloud_sync_timer:
            self.cloud_sync_timer.stop()

    def open_settings(self):
        dialog = QDialog(self)
        dialog.setWindowTitle("设置")
        dialog.resize(500, 600)
        layout = QVBoxLayout(dialog)
        general_group = QGroupBox("常规设置")
        general_layout = QFormLayout()
        auto_save = QCheckBox("启用自动保存")
        auto_save.setChecked(self.config["auto_save"])
        auto_save.toggled.connect(self.toggle_auto_save)
        general_layout.addRow("自动保存:", auto_save)
        auto_backup = QCheckBox("启用自动备份")
        auto_backup.setChecked(self.config["auto_backup"])
        auto_backup.toggled.connect(self.toggle_auto_backup)
        general_layout.addRow("自动备份:", auto_backup)
        cloud_sync = QCheckBox("启用云同步")
        cloud_sync.setChecked(self.config["cloud_sync"])
        cloud_sync.toggled.connect(self.toggle_cloud_sync)
        general_layout.addRow("云同步:", cloud_sync)
        backup_versions = QSpinBox()
        backup_versions.setRange(1, 20)
        backup_versions.setValue(self.config["backup_versions"])
        backup_versions.valueChanged.connect(lambda v: self.config.update({"backup_versions": v}))
        general_layout.addRow("备份版本数量:", backup_versions)
        general_group.setLayout(general_layout)
        layout.addWidget(general_group)
        editor_group = QGroupBox("编辑器设置")
        editor_layout = QFormLayout()
        font_combo = QComboBox()
        font_combo.addItems([
            "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "SimHei",
            "Arial", "Times New Roman", "Courier New", "Georgia", "Verdana"
        ])
        font_combo.setCurrentText(self.config["font_family"])
        font_combo.currentTextChanged.connect(self.change_font)
        editor_layout.addRow("字体:", font_combo)
        upload_font_layout = QHBoxLayout()
        self.upload_font_btn = QPushButton("上传字体文件")
        self.upload_font_btn.clicked.connect(self.upload_custom_font)
        self.current_font_label = QLabel()
        if self.config["font_family"] not in [
            "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "SimHei",
            "Arial", "Times New Roman", "Courier New", "Georgia", "Verdana"
        ]:
            self.current_font_label.setText(f"当前字体: {os.path.basename(self.config['font_family'])}")
        else:
            self.current_font_label.setText("当前字体: (无自定义字体)")
        upload_font_layout.addWidget(self.upload_font_btn)
        upload_font_layout.addWidget(self.current_font_label)
        editor_layout.addRow("自定义字体:", upload_font_layout)
        font_size = QDoubleSpinBox()
        font_size.setRange(8, 72)
        font_size.setValue(self.config["font_size"])
        font_size.valueChanged.connect(self.change_font_size)
        editor_layout.addRow("字体大小:", font_size)
        line_spacing = QDoubleSpinBox()
        line_spacing.setRange(1.0, 3.0)
        line_spacing.setSingleStep(0.1)
        line_spacing.setValue(self.config["line_spacing"])
        line_spacing.valueChanged.connect(lambda v: self.config.update({"line_spacing": v}))
        editor_layout.addRow("行间距:", line_spacing)
        editor_group.setLayout(editor_layout)
        layout.addWidget(editor_group)
        advanced_group = QGroupBox("高级设置")
        advanced_layout = QFormLayout()
        auto_save_interval = QSpinBox()
        auto_save_interval.setRange(1, 60)
        auto_save_interval.setValue(self.config["auto_save_interval"] // 60000)
        auto_save_interval.setSuffix(" 分钟")
        auto_save_interval.valueChanged.connect(lambda m: self.config.update({"auto_save_interval": m * 60000}))
        advanced_layout.addRow("自动保存间隔:", auto_save_interval)
        advanced_group.setLayout(advanced_layout)
        layout.addWidget(advanced_group)
        btn_layout = QHBoxLayout()
        save_btn = QPushButton("保存")
        save_btn.clicked.connect(dialog.accept)
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(dialog.reject)
        btn_layout.addStretch()
        btn_layout.addWidget(save_btn)
        btn_layout.addWidget(cancel_btn)
        layout.addLayout(btn_layout)
        dialog.exec()

    def upload_custom_font(self):
        font_path, _ = QFileDialog.getOpenFileName(
            self, 
            "选择字体文件", 
            "", 
            "字体文件 (*.ttf *.otf *.ttc)"
        )
        if not font_path:
            return
        try:
            font_name = os.path.splitext(os.path.basename(font_path))[0]
            if not self.current_project_path:
                QMessageBox.warning(self, "警告", "请先打开或创建一个项目")
                return
            fonts_dir = os.path.join(self.current_project_path, "fonts")
            os.makedirs(fonts_dir, exist_ok=True)
            dest_path = os.path.join(fonts_dir, os.path.basename(font_path))
            if os.path.exists(dest_path):
                os.remove(dest_path)
            shutil.copy2(font_path, dest_path)
            self.config["font_family"] = dest_path
            if hasattr(self, 'custom_font_label'):
                self.custom_font_label.setText(os.path.basename(dest_path))
            if hasattr(self, 'current_font_label'):
                self.current_font_label.setText(f"当前字体: {os.path.basename(dest_path)}")
            from PySide6.QtGui import QFontDatabase
            font_id = QFontDatabase.addApplicationFont(dest_path)
            if font_id != -1:
                font_families = QFontDatabase.applicationFontFamilies(font_id)
                if font_families:
                    actual_font_name = font_families[0]
                    self.editor.setFont(QFont(actual_font_name, self.config["font_size"]))
                else:
                    self.editor.setFont(QFont("Microsoft YaHei", self.config["font_size"]))
            else:
                self.editor.setFont(QFont("Microsoft YaHei", self.config["font_size"]))
            QMessageBox.information(self, "成功", f"字体 '{font_name}' 已成功加载")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法加载字体文件:\n{str(e)}")

    def change_font(self, font_name):
        preset_fonts = [
            "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "SimHei",
            "Arial", "Times New Roman", "Courier New", "Georgia", "Verdana"
        ]
        if font_name in preset_fonts:
            self.config["font_family"] = font_name
            self.editor.setFont(QFont(font_name, self.config["font_size"]))
            if hasattr(self, 'custom_font_label'):
                self.custom_font_label.setText("")
            if hasattr(self, 'current_font_label'):
                self.current_font_label.setText("当前字体: (无自定义字体)")
        else:
            self.config["font_family"] = font_name
            self.editor.setFont(QFont(font_name, self.config["font_size"]))

    def change_font_size(self, size):
        self.config["font_size"] = size
        self.editor.setFont(QFont(self.config["font_family"], size))

    def format_text(self, format_type, checked):
        cursor = self.editor.textCursor()
        if cursor.hasSelection():
            fmt = cursor.charFormat()
            if format_type == "bold":
                fmt.setFontWeight(QFont.Bold if checked else QFont.Normal)
            elif format_type == "italic":
                fmt.setFontItalic(checked)
            elif format_type == "underline":
                fmt.setFontUnderline(checked)
            cursor.setCharFormat(fmt)

    def init_word_highlight(self):
        self.highlight_words = []
        self.highlight_format = QTextCharFormat()
        self.highlight_format.setBackground(QBrush(QColor(self.config["highlight_color"])))

    def toggle_word_highlight(self, enabled):
        self.config["word_highlight"] = enabled
        if enabled:
            words, ok = QInputDialog.getText(
                self, "关键词高亮", "输入要高亮的关键词（逗号分隔）:",
                text=",".join(self.highlight_words)
            )
            if ok and words:
                self.highlight_words = [w.strip() for w in words.split(",") if w.strip()]
                self.apply_word_highlight()
        else:
            self.clear_word_highlight()

    def apply_word_highlight(self):
        if not self.highlight_words:
            return
        cursor = QTextCursor(self.editor.document())
        cursor.beginEditBlock()
        for word in self.highlight_words:
            pattern = re.compile(re.escape(word), re.IGNORECASE)
            cursor.setPosition(0)
            while True:
                cursor = self.editor.document().find(pattern, cursor)
                if cursor.isNull():
                    break
                cursor.setCharFormat(self.highlight_format)
        cursor.endEditBlock()

    def clear_word_highlight(self):
        cursor = QTextCursor(self.editor.document())
        cursor.select(QTextCursor.Document)
        fmt = cursor.charFormat()
        fmt.setBackground(QBrush(Qt.NoBrush))
        cursor.setCharFormat(fmt)

    def init_reading_mode(self):
        self.reading_mode_active = False
        self.reading_mode_widget = QWidget(self)
        self.reading_mode_widget.setStyleSheet("""
            background-color: #f5f5f5;
            color: #333;
            font-family: SimSun;
            font-size: 16pt;
            padding: 40px;
        """)
        self.reading_mode_widget.hide()
        layout = QVBoxLayout(self.reading_mode_widget)
        self.reading_mode_text = QTextEdit()
        self.reading_mode_text.setReadOnly(True)
        self.reading_mode_text.setStyleSheet("""
            border: none;
            background-color: transparent;
            font-family: SimSun;
            font-size: 16pt;
            line-height: 1.8;
        """)
        layout.addWidget(self.reading_mode_text)
        close_btn = QPushButton("退出阅读模式 (Esc)")
        close_btn.clicked.connect(self.toggle_reading_mode)
        layout.addWidget(close_btn)

    def toggle_reading_mode(self, checked=None):
        if checked is None:
            checked = not self.reading_mode_active
        self.reading_mode_active = checked
        if checked:
            self.reading_mode_text.setPlainText(self.editor.toPlainText())
            self.reading_mode_widget.setGeometry(self.geometry())
            self.reading_mode_widget.show()
            self.reading_mode_text.setFocus()
        else:
            self.reading_mode_widget.hide()
            self.editor.setFocus()

    def keyPressEvent(self, event):
        if self.reading_mode_active and event.key() == Qt.Key_Escape:
            self.toggle_reading_mode(False)
        super().keyPressEvent(event)

    def show_outline(self):
        if not self.current_project_path:
            return
        proj_file = os.path.join(self.current_project_path, "project.json")
        with open(proj_file, "r", encoding="utf-8") as f:
            meta = json.load(f)
        chapters = sorted(meta.get("chapters", []), key=lambda x: x["index"])
        dialog = QDialog(self)
        dialog.setWindowTitle("章节大纲")
        dialog.resize(400, 500)
        layout = QVBoxLayout(dialog)
        outline_list = QListWidget()
        for chap in chapters:
            outline_list.addItem(f"第{chap['index']}章 {chap['title']}")
        layout.addWidget(outline_list)
        btn_layout = QHBoxLayout()
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(dialog.close)
        btn_layout.addStretch()
        btn_layout.addWidget(close_btn)
        layout.addLayout(btn_layout)
        dialog.exec()

    def check_duplicates(self):
        text = self.editor.toPlainText()
        words = re.findall(r'\w+', text.lower())
        word_count = {}
        for word in words:
            if len(word) > 3:
                word_count[word] = word_count.get(word, 0) + 1
        duplicates = [word for word, count in word_count.items() if count > 5]
        if duplicates:
            msg = "发现可能的重复词:\n" + ", ".join(duplicates[:10])
            if len(duplicates) > 10:
                msg += f"\n(共发现 {len(duplicates)} 个重复词，仅显示前10个)"
            QMessageBox.warning(self, "重复内容检测", msg)
        else:
            QMessageBox.information(self, "重复内容检测", "未发现明显重复内容。")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = NovelWriterApp()
    window.show()
    sys.exit(app.exec())